package com.example.mykotlin.controller

import cn.dev33.satoken.secure.BCrypt
import cn.dev33.satoken.stp.StpUtil
import com.example.mykotlin.base.auth.InitAuth
import com.example.mykotlin.base.auth.PoiuyCipher
import com.example.mykotlin.base.result.R
import com.example.mykotlin.base.result.ServiceException
import com.example.mykotlin.base.util.Audit
import com.example.mykotlin.base.util.Constant
import com.example.mykotlin.base.util.GetBean
import com.example.mykotlin.base.util.lockService
import com.example.mykotlin.base.util.runService
import com.example.mykotlin.base.util.selectById
import com.example.mykotlin.base.util.tryOnly
import com.example.mykotlin.base.util.updateById
import com.example.mykotlin.model.dto.LoginByUsernameDTO
import com.example.mykotlin.model.dto.OpenSafeDTO
import com.example.mykotlin.model.entity.User
import com.example.mykotlin.model.vo.TokenVO
import com.example.mykotlin.service.AsyncEventService
import com.mybatisflex.kotlin.extensions.db.deleteById
import com.mybatisflex.kotlin.extensions.db.filterOne
import com.mybatisflex.kotlin.extensions.db.insert
import com.mybatisflex.kotlin.extensions.kproperty.eq
import com.mybatisflex.kotlin.extensions.vec.vecOf
import com.mybatisflex.kotlin.vec.count
import com.mybatisflex.kotlin.vec.filter
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import java.time.LocalDateTime
import java.util.concurrent.TimeUnit
import org.springframework.beans.factory.annotation.Value
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@Tag(name = "用户认证Controller", description = "相关接口")
@RestController
@Validated
@RequestMapping("/auth")
class AuthController(
    private val asyncEventService: AsyncEventService,
) {
    @Value("\${sa-token.timeout:3600}")
    private var timeOut: Long = 3600

    companion object {
        // 锁定时间，单位秒
        private const val LOCK_TIME = 300L
        private const val MAX_FAIL_TIME = 5
        const val DEVICE = "DEVICE"
    }

    @PostMapping("/loginByUsername")
    @Operation(summary = "用户通过用户名密码登录", description = "返回登录后的token信息")
    fun loginByUsername(
        @RequestHeader token: String, @RequestBody @Validated user: LoginByUsernameDTO
    ) = runService {
        val userInfo = filterOne<User> { User::username eq user.username }
            ?: throw ServiceException("用户[${user.username}]不存在", "1")
        val now = LocalDateTime.now()
        // 判断锁定时间
        if (userInfo.lockoutEndsTime != null && userInfo.lockoutEndsTime!!.isAfter(now)) {
            throw ServiceException(
                "用户[${user.username}]被锁定，于${userInfo.lockoutEndsTime!!.format(Constant.DTF)}解除锁定"
            )
        }
        val password = PoiuyCipher.rsaDecrypt(user.password)
        val device: String = PoiuyCipher.rsaDecryptDevice(token)
        if (BCrypt.checkpw(password, userInfo.password)) {
            if (StpUtil.isLogin() && userInfo.id == StpUtil.getLoginId().toString().toLong() &&
                device == GetBean.redisTemplate().opsForValue()["${DEVICE}:${user.device}:${userInfo.id}"]?.toString()
            ) {
                val tokenValue = StpUtil.getTokenValue()
                StpUtil.setTokenValue(tokenValue)
                GetBean.redisTemplate().expire("${DEVICE}:${user.device}:${userInfo.id}", timeOut, TimeUnit.SECONDS)
            } else {
                StpUtil.logout(userInfo.id, user.device)
                StpUtil.login(userInfo.id, user.device)
                StpUtil.getSession()["username"] = user.username
                GetBean.redisTemplate().opsForValue()
                    .set("${DEVICE}:${user.device}:${userInfo.id}", device, timeOut, TimeUnit.SECONDS)
            }
            val userLoginTime = User(userInfo.id)
            userLoginTime.lastLoginTime = now
            updateById(userLoginTime)
            return@runService TokenVO(StpUtil.getTokenValue())
        } else {
            // 密码错误，根据上次登录时间判断是否增加短时间内的错误次数
            val userKey = "loginFail:${userInfo.id}"
            lockService(userKey, 10, 30) {
                val count = GetBean.redisTemplate().opsForValue()[userKey]
                if (count == null) {
                    GetBean.redisTemplate().opsForValue().set(userKey, 1, LOCK_TIME, TimeUnit.SECONDS)
                } else {
                    if ((count as Int) < MAX_FAIL_TIME) {
                        tryOnly {
                            GetBean.redisTemplate().opsForValue().increment(userKey)
                        }
                    } else {
                        val lockoutTime = User(userInfo.id)
                        lockoutTime.lockoutEndsTime = now.plusSeconds(LOCK_TIME)
                        updateById(lockoutTime)
                    }
                }
            }
            throw ServiceException("密码错误")
        }
    }

    @PostMapping("/registerAndLogin")
    @Operation(summary = "用户注册并登录", description = "返回登录后的token信息")
    fun registerAndLogin(
        @RequestHeader token: String, @RequestBody @Validated registerUser: LoginByUsernameDTO
    ) = runService {
        val rsaDecrypt = PoiuyCipher.rsaDecrypt(registerUser.password)
        if (rsaDecrypt.length !in 6..36) throw ServiceException("密码长度应在6到36位")
        val password = BCrypt.hashpw(rsaDecrypt)
        val user = User()
        user.username = registerUser.username
        user.nickname = registerUser.username
        user.avatar = registerUser.username
        user.password = password
        if (vecOf<User>().filter { User::username eq user.username!! }
                .count() > 0L) throw ServiceException("该用户名被抢先注册了")
        if (insert(user) > 0) asyncEventService.insertUserEvent(user.id!!)
        loginByUsername(token, registerUser)
    } as R<TokenVO>

    @GetMapping("/userLoginInfo")
    @Operation(summary = "获取用户登录信息", description = "返回用户登录信息")
    fun getLoginInfo() = runService {
        selectById<User>(StpUtil.getLoginId().toString().toLong())
    }

    @DeleteMapping("/userLoginInfo")
    @Operation(summary = "用户退出登录", description = "用户退出登录")
    fun deleteUserLoginInfo() = runService {
        StpUtil.logout(StpUtil.getLoginId(), StpUtil.getLoginDevice())
    }


    @DeleteMapping("/user")
    @Operation(summary = "注销账号信息", description = "删除用户信息")
    @Audit
    fun deleteUser() = runService {
        val userId = StpUtil.getLoginId().toString().toLong()
        if (InitAuth.isSystemAdminUser(userId)) throw ServiceException("该账号为系统账号，无法注销")
        if (!StpUtil.isSafe()) throw ServiceException("注销账号信息，请二级认证后再次访问")
        return@runService if (deleteById<User>(userId) > 0) {
            asyncEventService.deleteUserEvent(userId)
            true
        } else throw ServiceException("用户[${StpUtil.getSession()["username"]}]注销失败")
    }


    @PostMapping("/openSafe")
    @Operation(summary = "二级认证", description = "进行二级认证")
    fun openSafe(
        @RequestBody @Validated psd: OpenSafeDTO
    ) = runService {
        val userId = StpUtil.getLoginId().toString().toLong()
        val userInfo = selectById<User>(userId)!!
        val now = LocalDateTime.now()
        // 判断锁定时间
        if (userInfo.lockoutEndsTime != null && userInfo.lockoutEndsTime!!.isAfter(now)) {
            throw ServiceException(
                "用户[${userInfo.username}]被锁定，于${userInfo.lockoutEndsTime!!.format(Constant.DTF)}解除锁定"
            )
        }
        val password = PoiuyCipher.rsaDecrypt(psd.password)
        if (BCrypt.checkpw(password, userInfo.password)) {
            // 比对成功，为当前会话打开二级认证，有效期为120秒
            StpUtil.openSafe(120)
            "为当前会话打开二级认证，有效期为120秒"
        } else {
            // 密码错误，根据上次登录时间判断是否增加短时间内的错误次数
            val userKey = "loginFail:${userInfo.id}"
            lockService(userKey, 10, 30) {
                val count = GetBean.redisTemplate().opsForValue()[userKey]
                if (count == null) {
                    GetBean.redisTemplate().opsForValue().set(userKey, 1, LOCK_TIME, TimeUnit.SECONDS)
                } else {
                    if ((count as Int) < MAX_FAIL_TIME) {
                        tryOnly {
                            GetBean.redisTemplate().opsForValue().increment(userKey)
                        }
                    } else {
                        val lockoutTime = User(userInfo.id)
                        lockoutTime.lockoutEndsTime = now.plusSeconds(LOCK_TIME)
                        updateById(lockoutTime)
                    }
                }
            }
            throw ServiceException("密码错误")
        }
    }

}